/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.handler.component;

import com.google.common.base.Preconditions;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.CoveringQuery;
import org.apache.lucene.search.LongValuesSource;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.WildcardQuery;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.SyntaxError;

import java.util.ArrayList;
import java.util.Collection;

/**
 * A custom {@linkplain QParserPlugin} which supports subset queries on a given Solr index.
 * This filter accepts the name of the field whose value should be used for subset matching
 * and the set against which subset queries are to be run ( as a comma separated string values).
 */
public class SubsetQueryPlugin extends QParserPlugin {
  public static final String SETVAL_PARAM_NAME = "set_value";
  public static final String SETVAL_FIELD_NAME = "set_field";
  public static final String COUNT_FIELD_NAME = "count_field";
  public static final String MISSING_VAL_ALLOWED = "allow_missing_val";
  public static final String WILDCARD_CHAR = "wildcard_token";

  @SuppressWarnings("rawtypes")
  @Override
  public void init(NamedList arg0) {
  }

  @Override
  public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
    return new QParser(qstr, localParams, params, req) {

      @Override
      public Query parse() throws SyntaxError {
        String fieldName = Preconditions.checkNotNull(localParams.get(SETVAL_FIELD_NAME));
        String countFieldName = Preconditions.checkNotNull(localParams.get(COUNT_FIELD_NAME));
        boolean allowMissingValues = Boolean.parseBoolean(Preconditions.checkNotNull(localParams.get(MISSING_VAL_ALLOWED)));
        String wildcardToken = localParams.get(WILDCARD_CHAR);

        LongValuesSource minimumNumberMatch = LongValuesSource.fromIntField(countFieldName);
        Collection<Query> queries = new ArrayList<>();

        String fieldVals = Preconditions.checkNotNull(localParams.get(SETVAL_PARAM_NAME));
        for (String v : fieldVals.split(",")) {
          queries.add(new TermQuery(new Term(fieldName, v)));
        }
        if (wildcardToken != null && !wildcardToken.equals("")) {
          queries.add(new TermQuery(new Term(fieldName, wildcardToken)));
        }
        if (allowMissingValues) {
          // To construct this query we need to do a little trick tho construct a test for an empty field as follows:
          // (*:* AND -fieldName:*) ==> parses as: (+*:* -fieldName:*)
          // It is a feature of Lucene that pure negative queries are not allowed (although Solr allows them as a top level construct)
          // therefore we need to AND with *:*
          // We can then pass this BooleanQuery to the CoveringQuery as one of its allowed matches.
          BooleanQuery.Builder builder = new BooleanQuery.Builder();
          builder.add(new BooleanClause(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD));
          builder.add(new BooleanClause(new WildcardQuery(new Term(fieldName, "*")), BooleanClause.Occur.MUST_NOT));

          queries.add(builder.build());
        }
        return new CoveringQuery(queries, minimumNumberMatch);
      }
    };
  }

}
